function convert(input) { alert('Error. Contact us if this keeps happening.'); document.getElementById('modal-contact').classList.add('show'); } window.ctMaxUploadFiles = 1; var _efState = { blob: null, fileName: 'video', duration: 0, busy: false, inited: false, downloadUrl: null, dragging: false, touched: false, stripToken: 0 }; function _efFormatTime(t) { var hundredths = Math.floor(Math.max(0, t || 0) * 100); var minutes = Math.floor(hundredths / 6000); var seconds = ((hundredths % 6000) / 100).toFixed(2); return minutes + ':' + (seconds.length < 5 ? '0' : '') + seconds; } // Big scrubber under the video: the browser's own seek bar is too easy to miss. function _efUpdateScrubber(t) { var handle = document.getElementById('ef-handle'); var track = document.getElementById('ef-track'); var time = document.getElementById('ef-scrub-time'); var d = _efState.duration; var pct = d > 0 ? Math.max(0, Math.min(100, (t / d) * 100)) : 0; if (handle) handle.style.left = pct + '%'; if (track) { track.setAttribute('aria-valuemax', d.toFixed(2)); track.setAttribute('aria-valuenow', t.toFixed(2)); track.setAttribute('aria-valuetext', _efFormatTime(t)); } if (time) time.textContent = _efFormatTime(t) + ' / ' + _efFormatTime(d); _efUpdateApplyLabel(t); } // The button names the frame it will save, so it follows the scrubber without needing instructions. function _efUpdateApplyLabel(t) { var btn = document.getElementById('ef-apply'); var mode = document.getElementById('ef-mode'); if (!btn) return; if (mode && mode.value === 'interval') btn.textContent = 'Extract & Download'; else btn.textContent = 'Download frame at ' + _efFormatTime(t); } function _efMarkTouched() { if (_efState.touched) return; _efState.touched = true; var handle = document.getElementById('ef-handle'); if (handle) handle.classList.remove('ef-pulse'); var hint = document.getElementById('ef-scrub-hint'); if (hint) hint.style.visibility = 'hidden'; } function _efResetScrubber() { _efState.touched = false; var handle = document.getElementById('ef-handle'); if (handle) handle.classList.add('ef-pulse'); var hint = document.getElementById('ef-scrub-hint'); if (hint) hint.style.visibility = ''; _efUpdateScrubber(0); } function _efSeekPreview(t) { var video = document.getElementById('ef-video'); if (!video || !_efState.duration) return; t = Math.max(0, Math.min(_efState.duration, t)); video.pause(); try { video.currentTime = t; } catch (_) { } _efUpdateScrubber(t); _efMarkTouched(); } // Thumbnail strip behind the scrubber, rendered from a second hidden video so the preview isn't disturbed. function _efBuildFilmstrip(url) { var strip = document.getElementById('ef-filmstrip'); if (!strip) return; strip.innerHTML = ''; var token = ++_efState.stripToken; var v = document.createElement('video'); v.muted = true; v.playsInline = true; v.preload = 'auto'; v.src = url; v.onloadeddata = function() { if (token !== _efState.stripToken) return; var d = v.duration; var vw = v.videoWidth, vh = v.videoHeight; if (!isFinite(d) || d <= 0 || !vw || !vh) return; var trackH = strip.clientHeight || 64; var thumbW = trackH * vw / vh; var count = Math.max(4, Math.min(16, Math.round((strip.clientWidth || 600) / thumbW))); var canvases = []; for (var i = 0; i < count; i++) { var c = document.createElement('canvas'); c.height = 72; c.width = Math.round(72 * vw / vh); strip.appendChild(c); canvases.push(c); } var idx = 0; function next() { if (token !== _efState.stripToken || idx >= count) { v.removeAttribute('src'); v.load(); return; } var t = (idx + 0.5) * d / count; _efSeekTo(v, t).then(function() { if (token !== _efState.stripToken) return; canvases[idx].getContext('2d').drawImage(v, 0, 0, canvases[idx].width, canvases[idx].height); idx++; next(); }).catch(function() { idx = count; next(); }); } next(); }; } function _efParseTimestamp(s) { if (!s) return 0; s = s.trim(); if (/^\d+(\.\d+)?$/.test(s)) return parseFloat(s); var m = s.match(/^(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/); if (m) { var h = parseInt(m[1] || '0', 10); var mm = parseInt(m[2], 10); var ss = parseFloat(m[3]); return h * 3600 + mm * 60 + ss; } var m2 = s.match(/^(\d+):(\d+(?:\.\d+)?)$/); if (m2) return parseInt(m2[1], 10) * 60 + parseFloat(m2[2]); return 0; } function processFile(blob, fileName) { _efState.blob = blob; _efState.fileName = fileName || 'video'; _efState.busy = false; _efReleaseDownloadUrl(); $('#file-drop-zone').addClass('collapsedDropZone'); (function(){ var $b=$('#file-drop-zone .upload-file-button'); var l=$b.data('change-label'); if(l) $b.text(l); })(); var ws = document.getElementById('ef-workspace'); if (ws) ws.style.display = 'block'; var quickDl = document.getElementById('ef-download'); if (quickDl) quickDl.style.display = 'none'; var video = document.getElementById('ef-video'); if (video) { var url = URL.createObjectURL(blob); _efState.duration = 0; _efResetScrubber(); video.src = url; video.onloadedmetadata = function() { _efState.duration = video.duration || 0; _efUpdateScrubber(video.currentTime || 0); _efBuildFilmstrip(url); }; } } function _efReleaseDownloadUrl() { if (_efState.downloadUrl) { try { URL.revokeObjectURL(_efState.downloadUrl); } catch (_) { } _efState.downloadUrl = null; } } function _efPublishDownload(blob, outputName) { _efReleaseDownloadUrl(); var url = URL.createObjectURL(blob); _efState.downloadUrl = url; add_file_output(url, outputName, 0); var quickDl = document.getElementById('ef-download'); if (quickDl) { quickDl.href = url; quickDl.download = outputName; quickDl.textContent = outputName; quickDl.style.display = 'inline-flex'; } var dl = document.createElement('a'); dl.href = url; dl.download = outputName; dl.style.display = 'none'; document.body.appendChild(dl); try { dl.click(); } catch (_) { fireEvent(dl, 'click'); } document.body.removeChild(dl); } function _efSeekTo(video, t) { return new Promise(function(resolve, reject) { var handled = false; var target = Math.max(0, t); if (isFinite(video.duration) && video.duration > 0) { target = Math.min(target, Math.max(0, video.duration - 0.01)); } var timeoutId = null; function cleanup() { if (timeoutId) clearTimeout(timeoutId); video.removeEventListener('seeked', onSeeked); video.removeEventListener('error', onErr); video.removeEventListener('loadeddata', onCanPlay); video.removeEventListener('canplay', onCanPlay); } function finish() { if (handled) return; handled = true; cleanup(); requestAnimationFrame(function() { resolve(); }); } function fail(reason) { if (handled) return; handled = true; cleanup(); reject(new Error(reason || ('Seek failed at ' + target.toFixed(2) + 's'))); } function onSeeked() { finish(); } function onErr() { fail('Seek failed at ' + target.toFixed(2) + 's'); } function onCanPlay() { if (Math.abs((video.currentTime || 0) - target) <= 0.02 && video.readyState >= 2) { finish(); } } video.addEventListener('seeked', onSeeked); video.addEventListener('error', onErr); video.addEventListener('loadeddata', onCanPlay); video.addEventListener('canplay', onCanPlay); timeoutId = setTimeout(function() { fail('Seek timed out at ' + target.toFixed(2) + 's'); }, 10000); if (Math.abs((video.currentTime || 0) - target) <= 0.02) { if (video.readyState >= 2) { finish(); return; } return; } try { video.currentTime = target; } catch (e) { fail('Seek failed at ' + target.toFixed(2) + 's'); } }); } function _efCanvasBlob(canvas, format) { return new Promise(function(resolve, reject) { var mime = format === 'jpg' ? 'image/jpeg' : 'image/png'; var quality = format === 'jpg' ? 0.95 : undefined; canvas.toBlob(function(b) { if (!b) reject(new Error('toBlob failed')); else resolve(b); }, mime, quality); }); } function _efBlobToArrayBuffer(blob) { return new Promise(function(resolve, reject) { var reader = new FileReader(); reader.onload = function(e) { resolve(e.target.result); }; reader.onerror = function() { reject(new Error('Could not read frame data')); }; reader.readAsArrayBuffer(blob); }); } function _efBinaryStringToBlob(s) { var bytes = new Uint8Array(s.length); for (var i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i) & 255; return new Blob([bytes], { type: 'application/zip' }); } function _efGenerateZipBlob(zip) { if (typeof zip.generateAsync === 'function') { return zip.generateAsync({ type: 'blob' }); } return new Promise(function(resolve, reject) { try { var out; try { out = zip.generate({ type: 'blob', compression: 'STORE', mimeType: 'application/zip' }); } catch (_) { try { out = zip.generate({ type: 'uint8array', compression: 'STORE' }); } catch (_) { out = zip.generate({ type: 'string', compression: 'STORE' }); } } if (out instanceof Blob) { resolve(out); } else if (out instanceof ArrayBuffer || out instanceof Uint8Array) { resolve(new Blob([out], { type: 'application/zip' })); } else if (typeof out === 'string') { resolve(_efBinaryStringToBlob(out)); } else { reject(new Error('ZIP generation failed')); } } catch (e) { reject(e); } }); } function _efApply() { if (!_efState.blob) { alert('Drop a video first.'); return; } if (_efState.busy) return; var mode = document.getElementById('ef-mode').value; var format = document.getElementById('ef-format').value; var status = document.getElementById('ef-status'); var btn = document.getElementById('ef-apply'); var video = document.getElementById('ef-video'); if (!video || !_efState.duration) { alert('Video metadata not loaded yet, please retry.'); return; } var times = []; if (mode === 'single') { var t = _efParseTimestamp(document.getElementById('ef-timestamp').value); if (t < 0 || t > _efState.duration) { alert('Timestamp is outside the video duration.'); return; } times.push(t); } else { var iv = parseFloat(document.getElementById('ef-interval').value); if (!isFinite(iv) || iv <= 0) { alert('Interval must be a positive number.'); return; } var maxFrames = 600; for (var t2 = 0; t2 < _efState.duration && times.length < maxFrames; t2 += iv) times.push(t2); if (times.length >= 50) { var warning = 'This will generate ' + times.length + ' images and may take a while or use a lot of memory.'; if (_efState.duration / iv > maxFrames) { warning += ' Only the first ' + maxFrames + ' frames will be extracted.'; } if (!confirm(warning + ' Continue? Choose Cancel to change the interval.')) return; } } if (btn) btn.disabled = true; _efState.busy = true; var w = video.videoWidth || 0; var h = video.videoHeight || 0; if (!w || !h) { alert('Video dimensions are unknown.'); _efState.busy = false; if (btn) btn.disabled = false; return; } video.pause(); _efReleaseDownloadUrl(); var quickDl = document.getElementById('ef-download'); if (quickDl) quickDl.style.display = 'none'; var canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; var ctx = canvas.getContext('2d'); var base = (_efState.fileName || 'video').replace(/\.[^.]+$/, ''); function pad(n, w) { var s = String(n); while (s.length < w) s = '0' + s; return s; } var doneSingle = times.length === 1; if (doneSingle) { _efSeekTo(video, times[0]).then(function() { ctx.drawImage(video, 0, 0, w, h); return _efCanvasBlob(canvas, format); }).then(function(blob) { var ts = times[0].toFixed(2).replace('.', '_'); _efPublishDownload(blob, base + '-' + ts + 's.' + format); if (status) status.textContent = 'Done. Download ready.'; _efState.busy = false; if (btn) btn.disabled = false; }).catch(function(err) { if (status) status.textContent = ''; _efState.busy = false; if (btn) btn.disabled = false; alert('Could not extract frame: ' + (err && err.message || err)); }); return; } loadScriptPromise('/js/jszip.min.js').then(function() { if (typeof JSZip === 'undefined') throw new Error('ZIP library failed to load'); var zip = new JSZip(); var idx = 0; var total = times.length; function next() { if (idx >= times.length) { if (status) status.textContent = 'Packaging ZIP...'; return _efGenerateZipBlob(zip).then(function(zipBlob) { var outputName = base + '-frames.zip'; _efPublishDownload(zipBlob, outputName); if (status) status.textContent = 'Done. Download ready.'; _efState.busy = false; if (btn) btn.disabled = false; }); } var t = times[idx]; if (status) status.textContent = 'Frame ' + (idx + 1) + ' of ' + total + '...'; return _efSeekTo(video, t).then(function() { ctx.drawImage(video, 0, 0, w, h); return _efCanvasBlob(canvas, format); }).then(function(blob) { var name = base + '-' + pad(idx + 1, 4) + '.' + format; return _efBlobToArrayBuffer(blob).then(function(ab) { zip.file(name, ab); idx++; return next(); }); }); } return next(); }).catch(function(err) { if (status) status.textContent = ''; _efState.busy = false; if (btn) btn.disabled = false; alert('Could not extract frames: ' + (err && err.message || err)); }); } function _efInit() { if (_efState.inited) return; _efState.inited = true; var video = document.getElementById('ef-video'); function syncTimestamp() { var timestamp = document.getElementById('ef-timestamp'); if (!timestamp || !video) return; timestamp.value = _efFormatTime(video.currentTime); if (!_efState.dragging) _efUpdateScrubber(video.currentTime || 0); } if (video) { video.addEventListener('timeupdate', syncTimestamp); video.addEventListener('seeking', syncTimestamp); video.addEventListener('seeked', syncTimestamp); video.addEventListener('loadedmetadata', syncTimestamp); // Using the native controls counts as finding the scrubber too. video.addEventListener('play', _efMarkTouched); } var track = document.getElementById('ef-track'); if (track) { function timeAt(clientX) { var rect = track.getBoundingClientRect(); var p = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); return p * _efState.duration; } track.addEventListener('pointerdown', function(e) { if (!_efState.duration) return; e.preventDefault(); track.focus(); _efState.dragging = true; try { track.setPointerCapture(e.pointerId); } catch (_) { } _efSeekPreview(timeAt(e.clientX)); }); track.addEventListener('pointermove', function(e) { if (_efState.dragging) _efSeekPreview(timeAt(e.clientX)); }); function endDrag() { _efState.dragging = false; } track.addEventListener('pointerup', endDrag); track.addEventListener('pointercancel', endDrag); track.addEventListener('keydown', function(e) { if (!video || !_efState.duration) return; var step = { ArrowLeft: -0.0334, ArrowRight: 0.0334, PageDown: -1, PageUp: 1 }[e.key]; if (e.key === 'Home') step = -_efState.duration; if (e.key === 'End') step = _efState.duration; if (step === undefined) return; e.preventDefault(); _efSeekPreview((video.currentTime || 0) + step); }); } Array.prototype.forEach.call(document.querySelectorAll('[data-ef-step]'), function(b) { b.addEventListener('click', function() { if (!video) return; _efSeekPreview((video.currentTime || 0) + parseFloat(b.getAttribute('data-ef-step'))); }); }); var tsInput = document.getElementById('ef-timestamp'); if (tsInput) tsInput.addEventListener('change', function() { _efSeekPreview(_efParseTimestamp(tsInput.value)); }); var mode = document.getElementById('ef-mode'); if (mode) mode.addEventListener('change', function() { document.getElementById('ef-interval-row').style.display = mode.value === 'interval' ? '' : 'none'; document.getElementById('ef-timestamp-row').style.display = mode.value === 'single' ? '' : 'none'; document.getElementById('ef-scrub').style.display = mode.value === 'single' ? '' : 'none'; _efUpdateApplyLabel(video ? (video.currentTime || 0) : 0); }); var apply = document.getElementById('ef-apply'); if (apply) apply.addEventListener('click', _efApply); } document.addEventListener('DOMContentLoaded', _efInit); if (document.readyState !== 'loading') _efInit(); var _loadedScripts = {}; function loadScriptPromise(url) { if (_loadedScripts[url]) return _loadedScripts[url]; _loadedScripts[url] = new Promise(function (resolve, reject) { var s = document.createElement('script'); s.src = url; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); return _loadedScripts[url]; } function replaceAll(find, replace, str) { return str.replace(new RegExp(find, 'g'), replace); } function beautify(str) { var result = ''; var length = str.length; var i = 0; var braceCountLeft = 0; var braceCountRight = 0; var withinQuotes = false; while (i < length) { var c = str[i]; if (c == '"' && (i == 0 || c[i - 1] != '\\')) { // non-escaped quotes withinQuotes = !withinQuotes; } if (!withinQuotes && (c == '}' || c == '{' || c == ',')) { console.log('Start####' + result); // look back and remove carriage returns and whitespace that are already there var resultIndex = result.length - 1; while (resultIndex >= 0 && (result[resultIndex] == ' ' || result[resultIndex] == '\r' || result[resultIndex] == '\n' || result[resultIndex] == '\t')) { resultIndex = resultIndex - 1; result = result.substr(0, resultIndex + 1); console.log('char ' + result[resultIndex] + '-----' + result + 'zzz ' + result.length + ' ' + resultIndex); } if (c == '{') { braceCountLeft++; result += c + '\r' + GetTabs(braceCountLeft - braceCountRight); } else if (c == '}') { braceCountRight++; // precede with carriage return result += '\r' + GetTabs(braceCountLeft - braceCountRight) + c; } else if (c == ',') { result += c + '\r' + GetTabs(braceCountLeft - braceCountRight); } var nextChar = ''; // advance through whitespace and remove carriage returns that are already there while (i < length && (str[i + 1] == ' ' || str[i + 1] == '\r' || str[i + 1] == '\n' || str[i + 1] == '\t')) { i++; } } else { result += str[i]; } i++; } return result; } function GetTabs(count) { var result = ''; for (var i = 0; i < count; i++) { result += ' '; } return result; }